Always-Valid Domain Model
要は
ドメインオブジェクトは、Validな状態でしか生成・存在できないようにしよう
というもの。
Validな状態でしか生成できないようにする
会員のドメインモデルを考える。属性としてemailAddressを持っていて、これは通知メールを送るのに使われる。
code:Member.java
public class Member {
String emailAddress;
}
code:RegisterMemberUseCase.java
public class RegisterMemberUseCase {
private SaveMemberPort saverMemberPort;
void reister(RegisterMemberCommand command) {
// これを呼び忘れると大問題
if (!command.getEmailAddress().matches("^\\S+@\\S+\\.\\S+$")) {
throw new IllegalArgumentException();
}
Member member = new Member(command.getEmailAddress());
saveMemberPort.save(member);
}
}
ドメインモデルを生成するのに不正なメールアドレスを渡さないようにする責務が利用側にある。
こうなると、バリデーションを忘れると不正なメールアドレスを持ったMemberオブジェクトが生成されてしまう。
code:java
class Member {
String emailAddress;
public boolean isValid() {
return emailAddress.matches("^\\S+@\\S+\\.\\S+$");
}
}
バリデーションをドメインオブジェクトに移したところで、isValidを呼ぶ責務は依然として呼び出し側にある。
そこで、そもそも不正な状態でオブジェクトを生成できないようにする。
code:Member.java
@Getter
public class Member {
String emailAddress;
public Member(String emailAddress) {
if (!emailAddress.matches("^\\S+@\\S+\\.\\S+$")) {
throws new IllegalArgumentException();
}
this.emailAddress = emailAddress;
}
}
属性がアトミックでないとAlways-Validにできないこともある
Eメールアドレスが検証済みでないと通知が送れないとする。これを実現するために先のモデルに、Eメールアドレスが検証済みかどうかのemailVerified属性を追加する。そして、検証用のサービスVerifyEmailServiceを用いて、検証がOKであれば、
code:java
@Getter
public class Member {
String emailAddress;
boolean emailVerified;
}
public interface VerifyEmailService {
boolean verify(String emailAddress);
}
public class VerifyEmailUseCase {
private VerifyEmailService verifyEmailService;
private SaveMemberPort saveMemberPort;
public void verify(VerifyEmailCommand command) {
if (verifyEmailService.verify(command.getEmailAddress())) {
Member member = new Member(command.getEmailAddress(), true);
saveMemberPort.save(member);
} else {
throw new FailToVerifyEmailException();
}
}
}
こうなるとEメール自体と、EメールをVerifyするアクションとその結果がバラバラになって、ビジネス的整合性を保つ責務が、またもや呼び出し側に行ってしまう。すなわちVerifyを呼び忘れると、Eメールアドレスが未検証なのに、検証済みフラグがONのMemberオブジェクトが作れてしまう。
これを防ぐには、未検証のEメールアドレス(UnverifiedEmailAddress)と検証済みEメールアドレス(VerifiedEmailAddress)の2つの型を定義する。検証済みEメールアドレスの型は、EメールのVerifyアクションを通じてしか生成できないようにし、メール送信機能においても検証済みEメールアドレスのみを受け付けるように書いておけば、誤って未検証Eメールアドレスにメール送信してしまう事故を防げる。
code:java
@Getter
public abstract class EmailAddress {
String value;
}
public class VerifiedEmailAddress extends EmailAddress {
VerifiedEmailAddress(String value) {
super(value);
}
}
public class UnverifiedEmailAddress extends EmailAddress {
public UnverifiedEmailAddress(String value) {
super(value);
}
}
public class Member {
EmailAddress emailAddress;
}
public interface VerifyEmailService {
VerifiedEmailAddress verify(UnverifiedEmailAddress emailAddress);
}
public interface SendNotificationService {
void send(VerifiedEmailAddress emailAddress);
}
というように、常にValidな状態を保つには、
なんでも入る緩い型
暗黙的な複数の属性の関連